Skip to content

Asynchronous Updates

Alright, time to cover our first state-based !

Consider the following code. What value would you expect to see in the developer console when the user clicks the button for the first time?

function App() {
const [count, setCount] = React.useState(0);
return (
<>
<p>
You've clicked {count} times.
</p>
<button
onClick={() => {
setCount(count + 1);
console.log(count)
}}
>
Click me!
</button>
</>
);
}

Pretty weird, right?

When we create our state variable, we initialize it to 0. Then, when we click the button, we increment it by 1, to 1. So shouldn't it log 1, and not 0?

Here's the catch: state setters aren't immediate.

When we call setCount, we tell React that we'd like to request a change to a state variable. React doesn't immediately drop everything; it waits until the current operation is completed (processing the click), and then it updates the value and triggers a re-render.

For now, the important thing to know is that updating a state variable is asynchronous. It affects what the state will be for the next render. It's a scheduled update.

Here's how to fix the code, so that we have access to the newer value right away:

function App() {
const [count, setCount] = React.useState(0);
return (
<>
<p>
You've clicked {count} times.
</p>
<button
onClick={() => {
const nextCount = count + 1;
setCount(nextCount);
console.log(nextCount)
}}
>
Click me!
</button>
</>
);
}

Instead of passing that expression directly into the state-setter function, setCount(count + 1), we're “saving” it by storing it in a variable. We can then use that variable in our console.log statements (or wherever else we need it).

I like to use the prefix “next”, like nextCount or nextUser, since it conveys that we're talking about the future value of the state, what it will be on the next render. Ultimately, though, that's just my own personal preference. You can name these variables whatever you like.

Why does it work this way?

Given that this is such a common stumbling block, it's worth asking why it's set up this way. Wouldn't it be simpler if React updated the state variables right away?

Let's talk about it.

Video Summary

In this video, we dig into how React works when working with multiple state variables.

Here's the code:

import React from 'react';
function App() {
const [user, setUser] = React.useState({ name: 'Alyssa' });
const [status, setStatus] = React.useState('ready');
const [confirmationMessage, setConfirmationMessage] = React.useState();
if (!user) {
return <p>{confirmationMessage}</p>;
}
return (
<button
onClick={() => {
setUser(null);
setStatus('initial');
setConfirmationMessage("You have been logged out.");
}}
>
Log Out
</button>
);
}
export default App;

When the user clicks the big “Log Out” button, here's what happens:

  1. The setUser function is called. React makes a note of this, assigning itself a metaphorical ticket. user will be changed to null.
  2. Then, on the next line, setStatus is called. React edits the Jira ticket, noting that on the next render, two state variables have to change.
  3. Finally, setConfirmationMessage is called. React edits the ticket again. It now knows the new value for all 3 state variables.

Once this onClick handler wraps up, React springs into action, performing all the steps we learned about in the “Core React Loop” lesson. It invokes the App function, user is initialized to null, and a paragraph is returned. React deletes the <button> DOM node, produces a new <p> DOM node, and the re-render is completed.

Now, let's imagine if state updates were synchronous.

When the user clicks the button, the click handler would be called, and setUser(null) would run. React would immediately do the re-render, performing all of the steps we talked about. It calls the App function, gets the result, destroys the button, creates a paragraph.

Only after all that, it resumes what it was doing in the onClick handler. And, immediately, another re-render is triggered for the status variable. And then, after all that, a third re-render for confirmationMessage.

We'd be forcing react to do 3x the amount of work, which would take 3x as long. So, it would be a performance liability. But it would also lead to inconsistent / broken UI!

For example: if we did a re-render after the first setUser call, user would be null, but confirmationMessage would still be undefined, since we haven't gotten there yet! As a result, we'd wind up with the following JSX:

<p>{undefined}</p>

Because state updates are asynchronous, they can be batched. React schedules the update, to take place as soon as the current work is completed (in practice, this is usually within a millisecond or two, so it feels completely instantaneous).

And so, while it definitely does cause a lot of confusion, especially for beginners, I think the React team made the right call.

Here's the sandbox from the video:

Code Playground

import React from 'react';

function App() {
const [user, setUser] = React.useState({ name: 'Alyssa' });
const [status, setStatus] = React.useState('ready');
const [confirmationMessage, setConfirmationMessage] = React.useState();

if (!user) {
return <p>{confirmationMessage}</p>;
}

return (
<button
onClick={() => {
setUser(null);
setStatus('initial');
setConfirmationMessage("You have been logged out.");
}}
>
Log Out
</button>
);
}

export default App;